<?php

namespace app\common\lib;

/**
 * redis + lua实现发号器服务
 */
class SnowflakeRedis
{
    const BITS_FULL = 64;
    const BITS_PRE = 1; //固定
    const BITS_TIME = 41; //毫秒时间戳 可以最多支持69年: 2^41-1/1000360024*365=69年
    const BITS_SERVER = 5; //服务器最多支持2^5=32台
    const BITS_WORKER = 5; //最多支持2^5=32种业务
    const BITS_SEQUENCE = 12; //一毫秒内支持2^12-1=4096个请求

    const OFFSET_TIME = "2022-06-10 00:00:00"; //时间戳起点时间

    /**
     * 服务器id
     */
    protected $serverId;

    /**
     * 业务id
     */
    protected $workerId;

    /**
     * 实例
     */
    protected static $instance;

    /**
     * redis 服务
     */
    protected static $redis;

    /**
     * 获取单个实例
     */
    public static function getInstance()
    {
        return self::$instance ?? (self::$instance = new self());
    }

    /**
     * 构造初始化实例
     */
    protected function __construct()
    {
        self::$redis = redis();
    }

    /**
     * 获取唯一值
     * @throws \Exception
     */
    public function nextId()
    {
        if (!isset($this->serverId)) {
            throw new \Exception("serverId is lost");
        }
        if (!isset($this->workerId)) {
            throw new \Exception("workerId is lost");
        }

        do {
            // 初始化发号器
            $id = pow(2, self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE;

            /*********** 为发号器添加时间属性 *************/
            //时间戳 41位
            $nowTime = (int)(microtime(true) * 1000);
            $startTime = (int)(strtotime(self::OFFSET_TIME) * 1000);
            //计算毫秒差
            $diffTime = $nowTime - $startTime;
            //计算出位移的偏移量
            $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
            //改变uuid的时间bit位
            $id |= $diffTime << $shift;

            /*********** 为发号器添加服务器属性 *************/
            //在新的$shift 计算出位移 的偏移量
            $shift = $shift - self::BITS_SERVER;
            //改变uuid的服务器bit位
            $id |= $this->serverId << $shift;

            /*********** 为发号器添加业务属性 *************/
            //在新的$shift 计算出位移 的偏移量
            $shift = $shift - self::BITS_WORKER;
            //改变uuid的业务编号bit位
            $id |= $this->workerId << $shift;

            /*********** 为发号器添加sequence *************/
            //自增值
            $sequenceNumber = $this->getSequence($id);
            if ($sequenceNumber > pow(2, self::BITS_SEQUENCE)) {
                usleep(1000);
            } else {
                $id |= $sequenceNumber;
                return $id;
            }
        } while (true);
    }

    /**
     * 反解获取业务数据
     */
    public function reverseNumber($number): array
    {
        $uuidItem = [];
        $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
        $uuidItem['diffTime'] = ($number >> $shift) & (pow(2, self::BITS_TIME) - 1);

        $shift -= self::BITS_SERVER;
        $uuidItem['serverId'] = ($number >> $shift) & (pow(2, self::BITS_SERVER) - 1);

        $shift -= self::BITS_WORKER;
        $uuidItem['workerId'] = ($number >> $shift) & (pow(2, self::BITS_WORKER) - 1);

        $shift -= self::BITS_SEQUENCE;
        $uuidItem['sequenceNumber'] = ($number >> $shift) & (pow(2, self::BITS_SEQUENCE) - 1);

        $time = (int)($uuidItem['diffTime'] / 1000) + strtotime(self::OFFSET_TIME);
        $uuidItem['generateTime'] = date("Y-m-d H:i:s", $time);

        return $uuidItem;
    }

    /**
     * 获取自增序列
     * @throws \ErrorException
     */
    protected function getSequence($id)
    {
        $lua = <<<LUA
        local sequenceKey = KEYS[1]
        local sequenceNumber = redis.call("incr", sequenceKey);
        redis.call("pexpire", sequenceKey, 1);
        return sequenceNumber
LUA;
        $sequence = self::$redis->eval($lua, [$id], 1);
        $luaError = self::$redis->getLastError();
        if (isset($luaError)) {
            throw new \ErrorException($luaError);
        }
        return $sequence;
    }

    /**
     * @return mixed
     */
    public function getServerId()
    {
        return $this->serverId;
    }

    /**
     * @param mixed $serverId
     */
    public function setServerId($serverId)
    {
        $this->serverId = $serverId;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getWorkerId()
    {
        return $this->workerId;
    }

    /**
     * @param mixed $workerId
     */
    public function setWorkerId($workerId)
    {
        $this->workerId = $workerId;
        return $this;
    }
}
